/*
 * alfviral is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * alfviral is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with Alfresco. If not, see <http://www.gnu.org/licenses/>.
 */
package com.fegor.alfresco.behavior;

import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Serializable;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketException;
import java.util.*;

import org.alfresco.service.cmr.repository.ContentIOException;
import org.alfresco.service.cmr.repository.ContentService;
import org.alfresco.service.cmr.repository.NodeService;
import org.alfresco.repo.content.ContentServicePolicies;
import org.alfresco.repo.policy.Behaviour;
import org.alfresco.repo.policy.Behaviour.NotificationFrequency;
import org.alfresco.repo.policy.JavaBehaviour;
import org.alfresco.repo.policy.PolicyComponent;
import org.alfresco.service.cmr.repository.NodeRef;
import org.alfresco.service.namespace.NamespaceService;
import org.alfresco.service.namespace.QName;
import org.apache.log4j.Logger;
import org.alfresco.model.ContentModel;
import org.alfresco.service.cmr.repository.ContentReader;


/**
 * Integrates antivirus scanning documents for alfresco
 * <p>
 * Implements the policies of "OnContentUpdate" and "OnContentRead".
 * <p>
 * The project is in: {@link} http://code.google.com/p/alfviral/
 * 
 * @author Fernando González (skype://fegorama)
 * @version 0.1
 */
public class OnUpdateReadScan 
    implements ContentServicePolicies.OnContentUpdatePolicy, 
                ContentServicePolicies.OnContentReadPolicy
{
    private Logger logger = Logger.getLogger(OnUpdateReadScan.class);

    // behaviours
    private Behaviour onContentUpdate;
    private Behaviour onContentRead;
    
    // dependencias
    private PolicyComponent policyComponent;
    private ContentService contentService;
    private NodeService nodeService;
    
    // configuration
    private String mode;
    private boolean on_update;
    private boolean on_read;
    
    // for mode COMMAND
    private List<String> command;
    private String store;
    
    // for mode INSTREAM
    public  int chunk_size = 4096;
    private int port;
    private String host;
    private int timeout;
    
    // aspect
    private final String NAMESPACE_ALFVIRAL_CONTENT_MODEL = "http://www.fegorsoft.com/model/alfviral/1.0";
    private final QName ASPECT_INFECTED = QName.createQName(NAMESPACE_ALFVIRAL_CONTENT_MODEL, "infected");
    private final QName PROP_INFECTED_DATE = QName.createQName(NAMESPACE_ALFVIRAL_CONTENT_MODEL, "date");
    private final QName PROP_INFECTED_CLEAN = QName.createQName(NAMESPACE_ALFVIRAL_CONTENT_MODEL, "clean");
    
    Map<QName,Serializable> aspectValues = new HashMap<QName,Serializable>();

    /**
     * Init method; policies definitions and bindings
     */
    public void init () 
    {
        if (logger.isDebugEnabled()) logger.debug("Start alfviral.OnUpdateReadScan");
        
        // create behavior and binding for updates
        if (this.on_update)
        {
            this.onContentUpdate = new JavaBehaviour(this, 
                    "onContentUpdate", 
                    NotificationFrequency.FIRST_EVENT);   
            
            this.policyComponent.bindClassBehaviour(QName.createQName
                    (NamespaceService.ALFRESCO_URI, 
                    "onContentUpdate"), 
                    "cm:content", this.onContentUpdate);            
        }
        
        // create behavior and binding for read
        if (this.on_read)
        {
            this.onContentRead = new JavaBehaviour(this, 
                    "onContentRead", 
                    NotificationFrequency.FIRST_EVENT);
            
            this.policyComponent.bindClassBehaviour(QName.createQName
                    (NamespaceService.ALFRESCO_URI, 
                    "onContentRead"), 
                    "cm:content", this.onContentRead);            
        }
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.repo.content.ContentServicePolicies.OnContentUpdatePolicy#onContentUpdate(org.alfresco.service.cmr.repository.NodeRef, boolean)
     */
    @Override
    public void onContentUpdate (NodeRef nodeRef, boolean flag) 
    {
        this.scanFile(nodeRef, "update");
    }
    
    /* (non-Javadoc)
     * @see org.alfresco.repo.content.ContentServicePolicies.OnContentReadPolicy#onContentRead(org.alfresco.service.cmr.repository.NodeRef)
     */
    @Override
    public void onContentRead (NodeRef nodeRef) 
    {
        if (nodeRef != null) this.scanFile(nodeRef, "read");
    }
   
    /**
     * Scan file for nodeRef
     * 
     * @param nodeRef
     * @param event
     */
    private void scanFile(NodeRef nodeRef, String event) 
    {
        int res = 0;
        ContentReader contentReader = this.contentService.getReader(nodeRef, ContentModel.PROP_CONTENT);
        
        if (contentReader != null)
        {
            if (logger.isDebugEnabled()) logger.info("["+event+"] Scaning nodeRef: "+nodeRef.getId()+" of "+contentReader.getContentUrl());
            
            // if mode is COMMAND
            if (mode.toUpperCase().equals("COMMAND"))
            {
                // full path of file
                String contentUrl = contentReader.getContentUrl();
                String contentPath = contentUrl.replaceFirst("store:/", this.store);
                res = this.scanCommand(contentPath);
    
            }
            // if mode is INSTREAM
            else if(mode.toUpperCase().equals("INSTREAM"))
            {
                try
                {
                  
                    res = this.scanSocket(contentReader.getContentString().getBytes());
                } 
                catch (ContentIOException e)
                {
                    logger.info("["+event+"] Not found content for nodeRef: "+nodeRef.getId()+" of "+contentReader.getContentUrl()+". ¿Is closed?: "+contentReader.isClosed());
                } 
                catch (IOException e)
                {
                    logger.error("["+event+"] Error in instream operation.", e);
                }   
            }
            else
            {
                if (logger.isDebugEnabled()) logger.debug("No config action: {COMMAND|INSTREAM}");            
            }
            
            // if res not zero... infected!!
            if (res != 0)
            {
                logger.info("["+event+"] ALERT ** File: "+contentReader.getContentUrl()+" is infected! **");                    
                this.addAspect(nodeRef);
            }
            else
            {
                if (logger.isDebugEnabled()) logger.debug("["+event+"] File: "+contentReader.getContentUrl()+" is clean");
            }
        }
    }
    
    /**
     * Add aspect Infected is not assigned
     * 
     * @param nodeRef
     */
    private void addAspect (NodeRef nodeRef)
    {
        if (!nodeService.hasAspect(nodeRef, this.ASPECT_INFECTED))
        {   
            this.aspectValues.put(this.PROP_INFECTED_DATE, new Date());
            this.aspectValues.put(this.PROP_INFECTED_CLEAN, false);                
            nodeService.addAspect(nodeRef, this.ASPECT_INFECTED, this.aspectValues);
        }
        else
        {
            if (logger.isDebugEnabled()) logger.debug("Este fichero se detectó como infectado anteriormente.");                    
        }        
    }
    
    /**
     * Scan file with command
     * 
     * @param contentPath
     * @return Is infected
     */
    private int scanCommand(String contentPath)
    {
        int res = 0;
        try
        {
            // execute command "script/command contentPath"
            this.command.add(contentPath);
            ProcessBuilder pb = new ProcessBuilder(this.command);
            Process process = pb.start();
            
            // result for process
            res = process.waitFor();
        } 
        catch (IOException e)
        {
            logger.error(" Error in execute command.", e);
        } 
        catch (InterruptedException e)
        {
            logger.error(" Error in execute command.", e);
        }
        return res;        
    }
    
// TODO Repasar todo este código...
    
    /**
     * Scan file with data stream for TCP port
     * 
     * @param data
     * @return Is infected
     * @throws IOException
     */
    private int scanSocket(byte[] data) 
                        throws IOException 
    {
        // counter and size
        int i = 0;
        
        // create socket
        Socket socket = new Socket();
        socket.connect(new InetSocketAddress(this.host, this.port));

        try 
        {
            socket.setSoTimeout(this.timeout);
        } 
        catch (SocketException e) 
        {
            logger.error("Error in timeout: " + this.timeout + "ms", e);
        }

        DataOutputStream dataOutputStream = null;
        BufferedReader bufferedReader = null;

        String res = null;
        try {
            dataOutputStream = new DataOutputStream(socket.getOutputStream());
            dataOutputStream.writeBytes("zINSTREAM\0");
            
            while (i < data.length)
            {
                if (i + this.chunk_size >= data.length)
                {
                    this.chunk_size = data.length - i;
                }
                dataOutputStream.writeInt(chunk_size);
                dataOutputStream.write(data, i, chunk_size);
                i += chunk_size;
            }

            dataOutputStream.writeInt(0);
            dataOutputStream.write('\0');
            dataOutputStream.flush();
            
            bufferedReader = new BufferedReader(new
                                    InputStreamReader(socket.getInputStream(), 
                                                        "ASCII"));

            res = bufferedReader.readLine();
        } 
        finally 
        {
            if (bufferedReader != null) bufferedReader.close();
            if (dataOutputStream != null) dataOutputStream.close(); 
            if (socket != null) socket.close(); 
        }
      
        // if is OK then not infected, else, infected...
        if (res.trim().equals("stream: OK"))
        {
            return 0;
        }
        else
        {
            return -1;
        }
    }

    /**
     * Visualize message "loaded"
     */
    public void loaded()
    {
        logger.info("Alfresco Virus Alert AMP has been loaded");
    }
    
    // Setters...
    public void setPolicyComponent(PolicyComponent policyComponent) 
    {
        this.policyComponent = policyComponent;
    }
    
    public void setContentService(ContentService contentService)
    {
        this.contentService = contentService;
    }
    
    public void setNodeService(NodeService nodeService)
    {
        this.nodeService = nodeService;
    }
    
    public void setStore(String store)
    {
        this.store = store;
    }
    
    public void setCommand(List<String> command) 
    {
        this.command = command;
    }    
    
    public void setMode(String mode)
    {
        this.mode = mode;
    }
    
    public void setHost (String host)
    {
        this.host = host;
    }
    
    public void setPort (int port)
    {
        this.port = port;
    }

    public void setTimeout(int timeout)
    {
        this.timeout = timeout;
    }
 
    public void setChunkSize(int chunk_size)
    {
        this.chunk_size = chunk_size;
    }
    
    public void setOnUpdate(boolean on_update)
    {
        this.on_update = on_update;
    }
    
    public void setOnRead(boolean on_read)
    {
        this.on_read = on_read;
    }
}
